<?php
/**
 * phpwell functional test class
 *
 * A test class based on simpletest but designed for functional testing
 *
 *
 * @package     phpwell
 * @author      Tom Atwell <klenwell@gmail.com>
 * @copyright   Copyright 2010, Tom Atwell <klenwell@gmail.com>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @link        http://klenwell.com/is
 *
 * <code>
 *  # phpwell base
 *  $phpwell_dir = dirname(dirname(dirname(__FILE__)));
 *  require_once(sprintf('%s/%s', $phpwell_dir, 'import.php'));
 *
 *  # imports
 *  import_well('test/functional');
 *  import_well('web/client');
 *
 *  # test class
 *  class FunctionalTest extends PhpwellFunctionalTestCase {
 *
 *      function start() {}
 *      function startUp() {}
 *      function run() {}
 *
 *      # TEST METHODS
 *
 *  }
 * </code>
 */
$phpwell_dir = dirname(dirname(__FILE__));
require_once(sprintf('%s/%s', $phpwell_dir, 'import.php'));


class PhpwellFunctionalTestCase {

    var $time_limit = 90;
    var $debug = 0;
    var $suppress_strict_error = TRUE;
    var $Log_ = array();
    var $ResultLog = array();
    var $FailLog = array();

    var $total_tests = 0;
    var $failed_tests = 0;

    var $_current_method = NULL;
    var $passed_last_test = FALSE;

    var $integrated_report = '';

    function __construct() {
        set_error_handler(array($this, '_error_handler'));
    }

    function __destruct() {
    }



    /**
     * basic methods
     */
    function startAll() {
        $this->log(sprintf('%s runs at start of all tests (not overridden)',
                           __FUNCTION__));
    }

    function endAll() {
        $this->log(sprintf('%s runs at end of all tests (not overridden)',
                           __FUNCTION__));
    }

    function startUp() {
        $this->log(sprintf('%s runs at start of each test (current method: %s)',
                           __FUNCTION__, $this->_current_method));
    }

    function tearDown() {
        $this->log(sprintf('%s runs at end of each test (current method: %s)',
                           __FUNCTION__, $this->_current_method));
    }

    function run() {
        $this->__run();
    }


    /**
     * error handler
     */
    function _error_handler($errno, $errstr, $errfile, $errline) {
        # skip STRICT errors
        if ( $this->suppress_strict_error && $errno == 2048 ) {
            return;
        }

        $this->ErrorList[] = $errstr;

        if ( ! in_array($errno, array(E_NOTICE, E_STRICT)) ) {
            $this->error_fail("ERROR #$errno $errstr");
        }
        else {
            $this->log("ERROR #$errno [$errfile:$errline] $errstr");
        }

        return TRUE;
    }

    function get_last_error() {
        return array_pop($this->ErrorList);
    }


    /**
     * run methods
     */
    function __run() {
        $this->start_time = $this->utime();
        $this->set_time_limit($this->time_limit);
        $this->startAll();
        $this->__run_each();
        $this->endAll();
        $this->total_tests = count(array_keys($this->FailLog));
        $this->failed_tests = count(array_keys($this->FailLog, 1));
        $this->passed_tests = $this->total_tests - $this->failed_tests;
        $this->end_time = $this->nf($this->utime() - $this->start_time);
    }

    function __run_each() {

        $TestMethods = $this->_get_test_methods();
        foreach ( $TestMethods as $method) {
            $this->__run_test($method);
        }
    }

    function __run_test($method) {
        $this->_current_method = $method;
        $this->startUp();
        $this->$method();
        $this->tearDown();
    }


    /**
     * helper methods
     */
    function assertTrue($pass, $m='') {
        return $this->assert($pass, $m, $stack_offset=2);
    }

    function assertEqual($val1, $val2, $m='') {
        $pass = ( $val1 == $val2 );
        if ( ! $pass ) {
            $this->log(sprintf('"%s" != "%s"', $val1, $val2));
        }
        return $this->assert($pass, $m, $stack_offset=2);
    }

    function assert($pass, $m='', $stack_offset=1) {
        $stackm1 = $this->caller($stack_offset);
        $stackm2 = $this->caller($stack_offset+1);
        if ( $pass )
        {
            $this->FailLog[] = 0;
            $this->ResultLog[] = array(
                'pass',
                $m,
                date('Y-m-d H:i:s'),
                $stackm1[0],
                $stackm1[1],
                $stackm2[2],
                $this->rsplit()
            );
        }
        else
        {
            $this->FailLog[] = 1;
            $this->ResultLog[] = array(
                'fail',
                $m,
                date('Y-m-d H:i:s'),
                $stackm1[0],
                $stackm1[1],
                $stackm2[2],
                $this->rsplit()
            );
        }
    }

    function pass($m='') {
        $stackm1 = $this->caller(1);
        $stackm2 = $this->caller(2);
        $this->FailLog[] = 0;
        $this->passed_last_test = 1;
        $this->ResultLog[] = array(
            'pass',
            $m,
            date('Y-m-d H:i:s'),
            $stackm1[0], $stackm1[1], $stackm2[2],
            $this->rsplit()
        );
        return 1;
    }

    function fail($m='') {
        $stackm1 = $this->caller(1);
        $stackm2 = $this->caller(2);
        $this->FailLog[] = 1;
        $this->passed_last_test = 0;
        $this->ResultLog[] = array(
            'fail',
            $m,
            date('Y-m-d H:i:s'),
            $stackm1[0], $stackm1[1], $stackm2[2],
            $this->rsplit()
        );
        return 0;
    }

    function error_fail($m='') {
        $stack = $this->caller(3);
        $stackm1 = $this->caller(4);
        $this->FailLog[] = 1;
        $this->passed_last_test = 0;
        $this->ResultLog[] = array(
            'ERROR',
            $m,
            date('Y-m-d H:i:s'),
            $stack[0], $stack[1], $stackm1[2],
            $this->rsplit()
        );
        return 0;
    }

    function log($m, $escape=0) {
        if ( is_array($m) ) {
            $m = $this->print_r($m);
        }

        if ( $escape ) {
            $m = "\n" . htmlentities($m);
        }

        $stackm1 = $this->caller(1);
        $stackm2 = $this->caller(2);

        $this->Log_[] = array(
            $m,
            date('Y-m-d H:i:s'),
            $stackm1[0],
            $stackm1[1],
            $stackm2[2],
            $this->lsplit()
        );
    }

    function set_time_limit($time_limit) {
        $this->log(sprintf('time limit set to %s s', $time_limit));
        set_time_limit($time_limit);
    }

    function see_log() {
        return sprintf('test failed -- see log in %s%s%s.log for details',
                        $root_dir, 'tmp/log/', date('Ymd'));
    }

    /**
     * A test I always like to add at the beginning just to get things rolling
     */
    function contains($haystack, $needle) {
        return $this->find_haystack_needle($haystack, $needle);
    }

    function find_haystack_needle($haystack, $needle) {
        return strpos($haystack, $needle) !== FALSE;
    }

    function is_instance($object, $class_name) {
        return is_a($object, $class_name);
    }

    /**
     * return array with function and line number going 2 back -- assumes
     * will be called by fail, pass, or log
     */
    function caller($back=2) {
        $Trace = debug_backtrace();
        #$this->pr($Trace);
        $f = basename($Trace[$back]['file']);
        $l = $Trace[$back]['line'];
        $fx = $Trace[$back]['function'];
        return array($f, $l, $fx);
    }

    function utime()
    {
        $utime = microtime();
        return $this->nf(substr($utime,0,9) + substr($utime,-10));
    }

    function lsplit()
    {
        static $last;
        $now = $this->utime();
        $last = !empty($last) ? $last : $now;
        $split = $this->nf($now - $last);
        $last = $now;
        return $split;
    }

    function rsplit()
    {
        static $last;
        $now = $this->utime();
        $last = !empty($last) ? $last : $now;
        $split = $this->nf($now - $last);
        $last = $now;
        return $split;
    }

    function nf($float, $dec=4)
    {
        return (float) number_format($float, $dec, '.', '');
    }

    function kill($html, $message='dying')
    {
        $this->print_d(htmlspecialchars($html), '#600');
        print $html;
        die($message);
    }

    function echo_d($message, $color='#ccc')
    {
        $_D = debug_backtrace();
        $f = basename($_D[0]['file']);
        $l = $_D[0]['line'];
        $loc = "{$f}:{$l}";
        $out = "<div style='line-height:1.5em; font-family:monospace; color:$color;'>$message <span style='color:#666;'>[$loc]</span></div>";
        $this->_DBG[] = "$loc -> " . strip_tags($message);
        echo $out;
        return;
    }

    function print_d($message, $color='#c33')
    {
        if ( $this->debug ) $this->echo_d($message, $color);
        return;
    }

    function print_r($Mixed)
    {
        $return = htmlspecialchars(print_r($Mixed, 1));
        $return = "<pre>$return</pre>";
        return $return;
    }

    function pr($Mixed, $header='')
    {
        if ( !empty($header) ) $header = "<b>$header</b>";
        $return = htmlspecialchars(print_r($Mixed, 1));
        $return = "<pre style='border:1px solid #ccc; background:#f3f3f3; padding:4px;'>$header\n\n$return</pre>";
        $this->echo_d($return, '#333');
    }

    function dump()
    {
        echo $this->print_r($this);
        return;
    }

    function _get_test_methods()
    {
        $TestMethodList = array();
        $class_name = get_class($this);
        $MethodList = get_class_methods($class_name);
        foreach ( $MethodList as $m )
        {
            if ( substr($m,0,4) != 'test' ) continue;
            $TestMethodList[] = $m;
        }
        return $TestMethodList;
    }

    /**
     * Reporter Methods
     */
    function report()
    {
        if ( IS_BROWSER_WELL ) {
            return $this->browser_report();
        }
        elseif ( IS_CLI_WELL ) {
            return $this->cli_report();
        }
        else {
            $this->log('environment (browser or cli) not detected');
            $report = $this->cli_report();
            return sprintf('<pre>%s</pre>', $report);
        }
    }

    function cli_report()
    {
        $plain_f = <<<XPLAIN
LOG
%s

RESULTS
%s

%s

tests complete

XPLAIN;

        # log
        $FormattedLog = array();
        foreach ($this->Log_ as $Log) {
            $FormattedLog[] = sprintf($log_t, $Log[1], $Log[3], $Log[2][0],
                                      $Log[2][1], $Log[0]);
        }

        # result log
        $FormattedResultLog = array();
        foreach ($this->ResultLog as $Log) {
            $FormattedResultLog[] = sprintf($rlog_t, $Log[0], $Log[0],
                                            $Log[3][2], $Log[4], $Log[1]);
        }

        $total_tests = count($this->ResultLog);
        $failed_tests = count(array_keys($this->FailLog, 0));
        $passed_tests = $total_tests - $failed_tests;
        $footer = sprintf('passed %s of %s tests in %s s',
                          $passed_tests,
                          $total_tests,
                          $this->end_time);

        return sprintf( $plain_f,
                        implode("\n", $FormattedLog),
                        implode("\n", $FormattedResultLog),
                        $footer );
    }

    function browser_report()
    {
        // log formatting
        $rlog_t = '<p class="%s"><b>%s &raquo; %s</b> [%s:%s] (%s) %s</p>';
        $log_t = '<p class="log">[%s (%s) &raquo; %s:%s] <b>%s</b></p>';

        // result log
        $ResultRows = array();
        foreach ($this->ResultLog as $Log) {
            $ResultRows[] = sprintf($rlog_t, $Log[0], $Log[0],
                                   $Log[5], $Log[3], $Log[4],
                                   $Log[6], $Log[1]);
        }

        // log
        $LogRows = array();
        foreach ($this->Log_ as $Log) {
            $LogRows[] = sprintf($log_t, $Log[1], $Log[4], $Log[2], $Log[3], $Log[0]);
        }

        // suite results
        $suite_result = ( array_sum($this->FailLog) > 0 ) ? 'fail' : 'pass';
        $suite_text = sprintf('passed %s of %s tests', $this->passed_tests, $this->total_tests);

        // footer
        $footer = "script completed in {$this->end_time} s";

        $html = <<<XHTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>
<head>
<title>Klenwell Functional Test: %s</title>
<style>
body { font-family:arial,sans-serif; }
h2 { margin:0 0 8px; }
h3 { padding:4px; margin:0 0 12px; font-size:1.2em; border-width:2px 0; border-style:solid; }
.fail { background:#ffeeee; color:#660000; border-color:#ff0000; }
.pass { background:#eeffee; color:#006600; border-color:#00ff00; }
.ERROR { background:#ffeeff; color:#660066; border-color:#ff00ff; }
p { margin:0; }
div.log_list { font-family:monospace; margin-bottom:8px; }
div .pass { margin:0; padding:3px; border-bottom:1px solid #00ff00; }
div .pass b { color:#00ff00; }
div .fail { margin:0; padding:3px; border-bottom:1px solid #ff0000; }
div .fail b { color:#cc0000; }
div .ERROR { margin:0; padding:3px; border-bottom:1px solid #ff00ff; }
div .ERROR b { color:#cc00cc; }
.log { margin:0; padding:3px;
 background:#eee; border-bottom:1px solid #ccc; color:#666;
}
h6.footer { color:#999; }
</style>
</head>
    <body>
        <h1>%s</h1>
        <h3 class="%s">%s &raquo; %s</h3>

        <div id="result_log">
            <h2>Tests</h2>
            <div class="log_list">%s</div>
        </div>

        <div id="log_log">
            <h2>Log</h2>
            <div class="log_list">%s</div>
        </div>

        <h6 class="footer">%s</h6>
    </body>
</html>
XHTML;

        return sprintf($html, __CLASS__, __CLASS__,
                        $suite_result,
                        ucwords($suite_result),
                        $suite_text,
                        implode("\n", $ResultRows),
                        implode("\n", $LogRows),
                        $footer);
    }
}


/**
 * main section: runs sample code below if this file is invoked directly in
 * browser or from command line
 */
$is_main_cli = isset($_SERVER['argv'][0]) && (realpath($_SERVER['argv'][0]) == __FILE__);
$is_main_browser= isset($_SERVER['SCRIPT_FILENAME']) && ($_SERVER['SCRIPT_FILENAME'] == __FILE__);
if ( $is_main_cli || $is_main_browser ) {

    class SamplePhpwellFunctionalTest extends PhpwellFunctionalTestCase {
        function testInstance() {
            $this->assert($this, 'SamplePhpwellFunctionalTest');
        }

        function testInstanceSuper() {
            $this->assert($this, 'PhpwellFunctionalTestCase');
        }

        function testAssertTrue() {
            $this->assertTrue(1);
        }

        function testAssertEqual() {
            $this->assertEqual(1, 1);
        }
    }

    $Test = &new SamplePhpwellFunctionalTest();
    $Test->log('this is basic demo of PhpwellFunctionalTestCase');
    $Test->run();
    print $Test->report();
}
?>
